#include "filesys.h"
#include "error.h"
#include <stdio.h>

#include "filedata.cpp"


class   TFile : IFile
    {
public:
    TFile() : FilePtr(nullptr), InternalFile(nullptr, NullDeleter) {};
    virtual ~TFile();
    virtual bool            OpenRead(string Filename);
    virtual cache_ptr       Load();
private:
    string      Filename;
    cache_ptr   InternalFile;
    FILE*       FilePtr;
    };

unique_ptr<IFile>   FileNew()
    {
    unique_ptr<IFile> Result((IFile*)new TFile);
    return Result;
    }

TFile::~TFile()
    {
    if(FilePtr)
        fclose(FilePtr);
    }

// keep constants in private namespace
namespace
    {
    const size_t    IO_SIZE         = (1024*16);        // grow by this much, as needed
    const size_t    FIRST_READ      = (IO_SIZE * 16);   // size of our (big!) first read attempt
    const long      MAX_FILE_SIZE   = (1024*1024*8);    // I doubt we could handle 8MB worth of grammar!
    }

#if 0
//??? fix to possibly check filesystem first
bool    TFile::OpenReadInternal(const char* Filename_)
    {
    Filename    = Filename_;
    const char* File = nullptr;
    for(auto f : FileIndex)
        if(!strcmp(f, "blaccstr.en"))
            {
            File    = f;
            break;
            }
    assert(File != nullptr);

    InternalFile.reset(File+strlen(File)+1);

    return true;
    
    }
#endif

/* OpenRead() - open a file for reading.
 *
 * This is somewhat clumsy because this function isolates access to the
 * "internal filesystem" (compiled in strings that represent data files).
 * If passed a filename that doesn't start with '$', then this function
 * simply tries to use fopen() to open that file.
 * If passed a filename that DOES start with '$', then caller wants us
 * to treat this as a localized file and, if -LocalDir=path was used,
 * check externally before using the internal, compiled-in version of the file.
 */
bool    TFile::OpenRead(string Filename_)
    {
    bool        Result  = false;
    const char* Path    = Filename_.c_str();

    assert(!Filename_.empty());
    DUMPVERBOSE("TFile::OpenRead(%s)", Path);
    if(*Path == '$')   // if it's an "internal" file
        {
        string  LocalePath;

        ++Path;            // skip over '$'
        if(!Global::LocaleDir.empty())   // if user specified explicit external dir to search
            {
            LocalePath  = Global::LocaleDir;
            LocalePath += Path;
            FilePtr = fopen(LocalePath.c_str(), "r");
            if(FilePtr != nullptr)  // if we found it in user-specified external dir
                {
                this->Filename  = LocalePath.c_str();
                Result          = true;
                }
            else
                DUMP("fopen(%s) failed\n", LocalePath.c_str());
            }
        if(Result == false) // either no external dir, or search there failed
            {               // so we will look internally!
            LocalePath  = Global::Locale;
            LocalePath += '/';
            LocalePath += Path;
            for(auto f : FileIndex)
                if(LocalePath == f)
                    {
                    // data is just past '\0' of filename
                    InternalFile.reset(f+strlen(f)+1);
                    break;
                    }
            if(InternalFile.get() != nullptr)
                Result  = true;
            else
                {
                Result  = false;
                DUMP("Could not load internal file '%s'\n", LocalePath.c_str());
                }
            }
        }
    // else, not an internal file, so use fopen()
    else
        {
        this->Filename  = Path;
        FilePtr         = fopen(Path, "r");
        Result          = (FilePtr != nullptr);
        }

    DUMPVERBOSE(" returns %s\n", Result ? "true" : "false");
    return Result;
    }

cache_ptr TFile::Load()
    {
    if(InternalFile.get())
        return cache_ptr {InternalFile.release(),InternalFile.get_deleter()};
    else if(FilePtr)
        {
        size_t  PhysicalSize    = (1024*256);

        // cache_ptr is const, so we'll use unique_ptr until we return
        unique_ptr<char,void(*)(const char*)>  Buffer {(char*)malloc(PhysicalSize), CacheDeleter};

        if(!Buffer.get())
            throw std::bad_alloc();

        assert(!Filename.empty());
        auto BytesRead = fread((void*)Buffer.get(), sizeof(char), PhysicalSize, FilePtr);

        if(BytesRead == PhysicalSize)
            Error("FILE_TOO_BIG", "Filename", Filename, "Size", PhysicalSize);
        /* check for errors first */
        else if(ferror(FilePtr))
            Error("FREAD_UNKNOWN_ERROR", "Filename", Filename);
        /* if we hit the end, then we're done */
        else if(feof(FilePtr))
            Buffer.get()[BytesRead]     = '\0';
        else
            Error("FREAD_FELL_SHORT", "Filename", Filename);

        // shrink it back down to minimum needed size.
        Buffer.reset((char*)realloc(Buffer.release(), BytesRead+1));
        if(!Buffer.get())
            throw std::bad_alloc();

        return cache_ptr{Buffer.release(),CacheDeleter};
        }
    else
        assert(false);

    return cache_ptr{nullptr,NullDeleter}; // pacify compiler; will never actually execute
    }
